{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 最终章 课程练习"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们终于迎来了实战的完结篇。这不仅是“**理解对象和类**”的完结篇，也是整个第一部分“**编程入门**”的完结篇，同时也是各位开始学习编程之后的第一个实习课题：**我们来实现一个简单的对话机器人**。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 理解问题"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在我们想编程实现一个什么时，一定要先问下自己：这个东西的实质是什么（*What is its nature*）？\n",
    "\n",
    "对话机器人的实质是什么呢？Siri 是个著名的对话机器人，我们可以和她闲聊，也可以问她问题和请她帮忙设个闹钟啊啥的："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"assets/siri.png\" width=\"300\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "对话机器人能够读取我们对它说的话（输入），理解其含义，然后做出合理的回应，然后把回应展示（输出）给我们。对话的主题可以各种各样，以 Siri 为例，主要可以分几类：\n",
    "* 通过特定指令让 Siri 操作手机完成特定任务（设闹钟、发邮件等）；\n",
    "* 提出特定领域的问题（*query*），Siri 搜索知识库并给出结果（问天气、问比赛结果、查地址等）；\n",
    "* 自由的闲聊，这个是最难的，因为缺乏上下文限定，计算机很难准确理解我们人类的真实意图。\n",
    "\n",
    "为了完成这些对话，Siri 包含了几个关键的部件：\n",
    "* 一个**语音识别**（*speech recognition*）引擎来把我们说的话转换成文字；\n",
    "* 一个**对话系统**（*dialog system*）来理解我们说的话的含义，并且尝试寻找合适的回应；\n",
    "* 一个巨大并且不断增长的**知识库**（*knowledge base*）后台，同时也是**语料库**（*text corpus*），帮助 Siri 找到并构造回应；\n",
    "* 将回应呈现反馈给我们，其中有文字有图片，也有从文字自动生成的语音，后面这个需要一个**文本合成语音**（*text-to-speech, TTS*）的引擎。\n",
    "\n",
    "这里面每一个都是人工智能领域的大课题，相对来说 *speech recognition* 和 *TTS* 这一对引擎比较成熟一些，虽然还有很多提升空间，比如 Siri 自动合成的语音平滑圆润，却还是缺乏真人的生动感（感情色彩），不过对于 Siri 这样的应用，效果已经足够好了。\n",
    "\n",
    "对话系统（*dialog system*）则是人工智能重要领域“**自然语言处理**（*natural language processing, NLP*）”中一个主要应用场景，其终极目标是创造能够与人自由对话（*free talk*）的计算机程序。\n",
    "\n",
    "自由对话已经被证明是极具挑战的，迄今为止，无论是 Apple 的 Siri，还是微软的 Cortana、微软小冰，或者 Google Now，或者小米的小爱同学，都还只是迈出了第一步，离终极目标还差得很远。不过对于特定场景或者特定目的的对话，这些试验产品都正在“开始变得有用”，其背后的技术进步目前已经开始应用在一些非常重要的系统中，比如大型客服系统。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 设计"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们自己要尝试实现一个对话机器人，当然不可能做到像 Siri 那么完整，目前能有一点对话系统的感觉就好，同时我们还希望搭好一个架子，让我们可以不断迭代加强这个系统的能力，可以这么去思考：\n",
    "* 简化输入和输出环节，用户直接输入文字，机器人直接回应文字；\n",
    "* 对系统进行功能分解，除了高度简化的输入和输出层，主要划分为“理解”和“回应”两个引擎；\n",
    "* 构造一个体系，让我们可以随时实现更好的“理解”和“回应”引擎并且无缝的替换。\n",
    "\n",
    "为了进一步降低难度，我们可以从程序发起的对话开始，也就是把“程序提问题-用户回应-程序再回应”作为一个基本对话单元，这比由用户自由提出话题要容易。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "程序设计不是画图纸，是像小孩玩沙子一样，一边尝试一边思考，在必要时才会画图纸。在做更复杂的思考和设计之前可以先随便的玩一下，看看在 Python 里怎么实现“程序提问题-用户回应-程序再回应”这样的基本单元，结合我们之前学过的知识，我们很快可以写出下面这样的代码："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 请在这里输入代码"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "是不是已经有点对话的样子了？我们可以再写一个类似的，这次引入一点逻辑判断和条件分支："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 请在这里输入代码"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在这里用各种输入测试几次，会发现一个小 bug：如果用户输入 *good* 或者 *I'm good* 这种，代码运行是如我们所想，但是如果用户输入的是 *Good*，程序会不认，而输出 *Sorry to hear that*，这显然不合理，源于我们代码中的不严谨，在判断时未考虑大小写问题。修正也很容易，改成下面这样就好了："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 请在这里输入代码"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "继续，这次我们引入一点随机性来增加乐趣："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 请在这里输入代码"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这里用了来自 `random` 模块中的 `choice` 函数，从列表 `colors` 中随机抽选一个元素。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "经过这一番尝试，我们可以有一些收获：\n",
    "* 每一轮交互都是 `print()` 一句话，然后用 `input()` 获取用户输入，然后围绕输入内容进行计算，给出一个相关的输出，并 `print()` 出来；\n",
    "* 在识别和处理输入时要当心大小写问题；\n",
    "* 如果细心的话还会发现体验上的一些细节，比如程序 `print()` 输出太快了，如果能停顿一下再输出感觉会更像对话。\n",
    "\n",
    "最重要的，我们知道基本的交互模式之后，只要愿意，我们可以重复上面的过程，写出好多好多交互剧本，和用户聊上一整天，但是那样好像很枯燥，而且要写好多重复的代码，所以现在我们可以停下来思考怎么把这个过程变得更方便和高效。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "回忆我们在前面几章所讲的一些编程的基本思维，尤其是“分而治之”和“责任分离”，把问题分解，每个子问题用一个程序模块来解决，每个程序模块力争把一个问题解决好，并且给出清晰的接口供其他模块使用。\n",
    "\n",
    "在对话机器人这个课题中，我们可以建立这么几个抽象模块：\n",
    "1. 每轮交互可以用一个 bot 对象来实现，不同的剧本实现为不同的 Bot 类；\n",
    "2. 每轮交互中的共性功能，比如输入输出的 `print-input-print` 流程，可以在一个公共的 Bot 父类中处理；\n",
    "2. 理解用户输入并给出回应是核心的逻辑，每个 Bot 类需要实现这个逻辑，但接口应该在 Bot 父类中统一。\n",
    "\n",
    "下面我们就来尝试构建这一设计的代码实现。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 构建"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "先来看公共父类 `Bot`："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 请在这里输入代码"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这个定义中我们可以看到前面尝试的成果已经很好的集成进来：\n",
    "* `run()` 方法就是一轮交互 `print-input-print` 流程的实现；\n",
    "* 前面我们提到的停顿一下的功能被加了进来，用的是 Python `time` 模块中的 `sleep()` 方法，这个方法让解释器休息指定的秒数；\n",
    "* 用一个类变量（*class variable*）`wait` 来管理程序说每句话之前要停顿的秒数；\n",
    "* 实例变量（*instance variable*）`self.q` 和 `self.a` 分别代表这一轮对话中程序提出的问题和用户的回答，是两个字符串；\n",
    "* `_think()` 方法就是从用户输入出发计算程序回答的核心算法，在 `run()` 方法中被调用，这个方法一般来说只在 `Bot` 类内部使用，外部最好不要直接访问，所以名字前面我们加了下划线 `_`。\n",
    "\n",
    "有了这个父类，我们就可以一股脑把前面尝试的几轮对话都实现为它不同的子类："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 请在这里输入代码"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 请在这里输入代码"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 请在这里输入代码"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以看到，具体对话逻辑的实现就两件事：\n",
    "1. 在 `__init__()` 方法中设定程序发问的问题是什么；\n",
    "2. 在 `_think()` 方法中根据用户输入来计算程序回应的内容。\n",
    "\n",
    "下面我们需要一个主流程把上面的这些 `Bot` 串起来，这是我们的对话系统的主类定义（我们给它起名叫 `Garfield`，你可以用你喜欢的名字）："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 请在这里输入代码"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这里面有点新东西，我们稍微解释一下：\n",
    "* 对话系统可以包含很多个对话 *bot*，我们把它们放在一个实例变量 `self.bots` 中，这是一个列表，就是我们在[介绍循环的一章](p1-5-structure-4.ipynb)提过的一种数据容器，初始化为空列表 `[]`；\n",
    "* 提供 `add()` 方法来往系统中加入 *bot*，加入方法是用列表的 `append()` 方法，如上代码中 `add(self, bot)` 方法定义；\n",
    "* 系统中各个 *bot* 的聊天延迟 `wait` 统一设定，在初始化对话系统时通过修改类变量 `Bot.wait` 的值来实现；\n",
    "* `run()` 方法是对话系统运行主方法，它先打印一行提示，然后开始一个一个的运行我们加入的 *bot*，即循环调用每个 `bot` 的 `run()` 方法。\n",
    "\n",
    "然后我们就可以初始化一个 `Garfield` 实例来具体运行了："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 请在这里输入代码"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这样我们的对话系统雏形就运行起来了，还有点像回事吧？\n",
    "\n",
    "以后我们可以定义更多的 *bot*，每个代表了一轮不一样的对话，然后加入到 `Garfield` 的对象实例，就可以运行起来了。\n",
    "\n",
    "你可以把这一节的代码一起放进一个源代码文件保存起来，然后在命令行用 `python garfield.py` 来运行一下。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 增量式优化"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "有了上面的第一版之后，我们可以随时“**增量式**（*progressively*）”更新和优化这个对话系统，比如：\n",
    "* 如果我们对交互的流程和样式不满意，就优化增强 `Bot` 父类的 `run()` 方法；\n",
    "* 如果我们需要更多对话内容，就定义更多不一样的 `Bot` 子类；\n",
    "* 如果想增加对话系统级的操作，比如提供系统帮助指令，可以修改 `Garfield` 和 `Bot` 类的 `run()` 方法来实现。\n",
    "\n",
    "这里我们给出两个增强的例子。第一个是给程序输出的对话加上颜色，以区分用户输入的内容，这个修改只涉及 `Bot` 父类的 `run` 方法（这就是责任分离的好处）。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "要给 Python `print()` 输出的文字加上颜色和其他格式，可以使用命令行特定的格式串，自己做起来有点复杂，我们用一个第三方的模块 `termcolor`，首先打开你的命令行界面运行 `pip install termcolor`，然后修改 `Bot` 父类定义如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 请在这里输入代码"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们增加了一个内部方法 `_format` 来对程序输出的文字进行格式化，在其中调用 `termcolor` 提供的 `colored` 函数来对字符串上色。程序的其他部分不需要改变，所有 `Bot` 子类继承父类自动得到这一新特性，你可以重新运行程序来检验一下。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "第二个例子是做一个新的 `Bot` 子类，可以对用户输入的一个四则运算表达式进行计算求值，也就是把对话系统变成一个计算器，你可以试着自己做一做，提示是：可以借助第三方库来进行表达式求值，比如这个 [simpleeval](https://pypi.org/project/simpleeval/)。\n",
    "\n",
    "下面是我们的参考答案。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "首先在命令行界面执行 `pip install simpleeval`，然后增加一个 `Bot` 子类的定义如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 请在这里输入代码"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "然后创建 `Garfield` 实例试试："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 请在这里输入代码"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "是不是比想象的还要简单啊？"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 作业"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在给你布置一个作业，我们想改进最后这个 `CalcBot`，让它可以反复执行，用户可以一直输入算术表达式求值，直到用户输入 `x` `q` `exit` 或者 `quit` 为止，才跳到下一个 *bot* 执行。\n",
    "\n",
    "提示：这个需求改变了 `run()` 方法的基本流程，所以需要在 `CalcBot` 中重新实现一个 `run()` 方法，覆盖掉父类中 `run()` 的实现。在本章最后有参考答案。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 小结"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们在本章实例中运用的程序设计方法叫做 *UDB*（[Understand, Design and Build](https://lob.com/blog/understand-design-build-a-framework-for-problem-solving)）：\n",
    "* *Understand*：深入理解问题，探求问题的本质，聚焦关键问题；\n",
    "* *Design*：通过初步尝试了解问题的基本解决方案，并作出责任分离的模块化设计，包括各个模块的职责、实现思路以及将各模块粘合起来的方法；\n",
    "* *Build*：编写代码实现每个模块，一边构建一边测试；尽快完成第一个版本，然后增量化持续改进。\n",
    "\n",
    "对于所有要解决问题的探索，这个方法基本都适用，格外适合编程，差别仅在问题的规模（难度和复杂度），你可以多多体会这个方法的思维模式，平时多尝试。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "本章还运用了前面学到的几乎所有东西，如果需要可以回到前面重新温习，直到可以顺利的完成本章的各个部分。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 附：作业参考答案"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "利用 `while` 循环和 `break` 语句很容易实现目标，修改 `CalcBot` 的 `run` 方法如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 请在这里输入代码"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这个版本已经可以完成作业了，但是还有更好的解决方案，就看你是不是够敏锐和有追求。\n",
    "\n",
    "仔细观察上面的代码，我们可以发现 `CalcBot` 真正的特性只在 `self.q` 和 `_think()` 方法中体现，而这个 `run()` 方法里面并没有任何和 `CalcBot` 的特性有关的内容，完全是通用的，也就是说如果想写个别的循环运行的 *bot*，完全可以复用这段代码。这强烈的提示我们，这段代码应该成为父类 `Bot` 的一部分。\n",
    "\n",
    "经过思考，我们可以给 `Bot` 父类一个新的实例变量 `runtype`，其值可以为 `'once'` 或者 `'loop'`，分别表示 *bot* 只运行一次或者循环运行，然后根据这个值来调用两个不同版本的 `run()` 方法。为此改写父类 `Bot` 如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 请在这里输入代码"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 请在这里输入代码"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们在新的 `__init__()` 方法中增加了一个参数 `runtype`，并指定其缺省值为 `'once'`，这意味着以前所写的只运行一次的 *bot* 们在实例化时的代码不需要修改，但这些子类的 `__init__()` 方法需要修改，对齐父类的定义，并调用父类的 `__init__()` 方法来初始化 `self.runtype`。所以老的子类要做如下修改："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 请在这里输入代码"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 请在这里输入代码"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 请在这里输入代码"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "注意上面的这几个子类的 `__init__()` 方法，里面调用了 `super()` 函数来获得父类对象，然后调用了父类的 `__init__()` 方法来做基本的初始化，而不需要自己再写一遍。在类定义中如果我们需要引用父类的一些实现，都可以用这个办法。\n",
    "\n",
    "而 `CalcBot` 可以大幅简化，就和上面这些大哥们一样了"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 请在这里输入代码"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "相应的，我们需要在实例化对话系统 `Garfield` 时指定哪些 *bot* 循环运行："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 请在这里输入代码"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "你做对了吗？如果你没看答案而给出了第一个方案，是完全合格的结果；如果给出了类似于第二个方案，你的编程思维已经非常了得，请千万不要浪费了这个天赋。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
